{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Decorator Classes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "I've already covered the topic of decorator classes, but let's review it quickly."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First off, don't confuse this with class decorators - here I'm talking about using a class to create decorators - that can be used to decorate functions, or classes - but instead of the decorator being a function, it is a class whose instances will act as decorators."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We do this by making instances of the decorator class **callable**, by implementing the `__call__` method."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's see a quick example of rewriting a regular decorator function into a decorator class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from functools import wraps\n",
    "\n",
    "def logger(fn):\n",
    "    @wraps(fn)\n",
    "    def wrapped(*args, **kwargs):\n",
    "        print(f'Log: {fn.__name__} called.')\n",
    "        return fn(*args, **kwargs)\n",
    "    return wrapped"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we can use this decorator to log function calls:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "@logger\n",
    "def say_hello():\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Log: say_hello called.\n"
     ]
    }
   ],
   "source": [
    "say_hello()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can rewrite this decorator function into a class, by making `__init__` take the function being decorated as an argument, and implementing the `__call__` method to actually run the original function (and output the log):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Logger:\n",
    "    def __init__(self, fn):\n",
    "        self.fn = fn\n",
    "        \n",
    "    def __call__(self, *args, **kwargs):\n",
    "        print(f'Log: {self.fn.__name__} called.')\n",
    "        return self.fn(*args, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "@Logger\n",
    "def say_hello():\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Log: say_hello called.\n"
     ]
    }
   ],
   "source": [
    "say_hello()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Remember also that the decorator syntax we used is the same as having done it this way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def say_hello():\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "function"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(say_hello)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "say_hello = Logger(say_hello)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Log: say_hello called.\n"
     ]
    }
   ],
   "source": [
    "say_hello()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But the **big** difference is that `say_hello` is no longer a function, but a **callable** object - an instance of the `Logger` class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "__main__.Logger"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(say_hello)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And this actually leads us to an issue.\n",
    "\n",
    "Let's try to use the same decorator to decorate methods in a class.\n",
    "\n",
    "We'll start with instance methods first."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        \n",
    "    @Logger\n",
    "    def say_hello(self):\n",
    "        return f'{self.name} says hello!'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person('David')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Log: say_hello called.\n"
     ]
    },
    {
     "ename": "TypeError",
     "evalue": "say_hello() missing 1 required positional argument: 'self'",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-14-b59e5dc06d54>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msay_hello\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;32m<ipython-input-4-f2eac18c7483>\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m      5\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      6\u001b[0m         \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf'Log: {self.fn.__name__} called.'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m         \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m: say_hello() missing 1 required positional argument: 'self'"
     ]
    }
   ],
   "source": [
    "p.say_hello()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "What's going on here? Why is Python complaining that `self` has not been passed to `say_hello`?\n",
    "\n",
    "We called it from an instance, so why is `self` not being passed to it."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Well, you have to remember what `say_hello` is now that it has been decorated - it is an instance of a class, not a function!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And do you remember how functions are turned into methods?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The descriptor protocol... Functions implement a `__get__` method, and that is ultimately used to create the bound method."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our class does not implement the `__get__` method, so that callable remain a plain callable, not a bound method, and that's why our implementation is broken."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<__main__.Logger at 0x7facc02c7b10>"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.say_hello"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But it's actually an easy fix, we can implement the `__get__` method in our class, to turn it into a (non-data) descriptor, just like a function does, and we just need to return a bound method."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Remember how we can create a method bound to an object.\n",
    "\n",
    "We can use `types.MethodType`. the first argument is the callable we want to bind, and the second argument is the instance we want to bind it to."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "from types import MethodType\n",
    "\n",
    "class Logger:\n",
    "    def __init__(self, fn):\n",
    "        self.fn = fn\n",
    "        \n",
    "    def __call__(self, *args, **kwargs):\n",
    "        print(f'Log: {self.fn.__name__} called.')\n",
    "        return self.fn(*args, **kwargs)\n",
    "    \n",
    "    def __get__(self, instance, owner_class):\n",
    "        print(f'__get__ called: self={self}, instance={instance}')\n",
    "        if instance is None:\n",
    "            print('\\treturning self unbound...')\n",
    "            return self\n",
    "        else:\n",
    "            # self is callable, since it implements __call__\n",
    "            print('\\treturning self as a method bound to instance')\n",
    "            return MethodType(self, instance)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        \n",
    "    @Logger\n",
    "    def say_hello(self):\n",
    "        return f'{self.name} says hello!'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person('David')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__get__ called: self=<__main__.Logger object at 0x7facc02c8610>, instance=<__main__.Person object at 0x7facc02c8750>\n",
      "\treturning self as a method bound to instance\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<bound method ? of <__main__.Person object at 0x7facc02c8750>>"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.say_hello"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see `say_hello` is now considered a bound method. And it bound the callable instance of Logger to the Person instance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__get__ called: self=<__main__.Logger object at 0x7facc02c8610>, instance=<__main__.Person object at 0x7facc02c8750>\n",
      "\treturning self as a method bound to instance\n",
      "Log: say_hello called.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'David says hello!'"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.say_hello()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can still use our `Logger` decorator class to decorate functions, since in that case `__get__` doesn't even come into play:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "@Logger\n",
    "def say_bye():\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<__main__.Logger at 0x7face0d1e850>"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "say_bye"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, the `__get__` method does not even get called."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The last thing we should check is that the decorator works with class and static methods too."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Just remember that the order of the decorators is important - we need to decorate with our logger before we decorate with the static and class decorators. that way we end up decorating the decorated function (so just a plain fuinction decorator), and then making it into a class or static method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    @classmethod\n",
    "    @Logger\n",
    "    def cls_method(cls):\n",
    "        print('class method called...')\n",
    "        \n",
    "    @staticmethod\n",
    "    @Logger\n",
    "    def static_method():\n",
    "        print('static method called...')\n",
    "        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Log: cls_method called.\n",
      "class method called...\n"
     ]
    }
   ],
   "source": [
    "Person.cls_method()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Log: static_method called.\n",
      "static method called...\n"
     ]
    }
   ],
   "source": [
    "Person.static_method()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
